import * as fs from "fs";
import * as path from "path";
import * as xlsx from "xlsx";
import * as morph from "ts-morph";
import { Dictionary } from "./Dictionary";

class FileHelper {

    static GetAllFiles(dir: string) {
        let files: string[] = [];
        this.GetAllFilesList(files, dir);
        return files;
    }

    private static GetAllFilesList(files: string[], dir: string) {
        const data = fs.readdirSync(dir);
        for (let i = 0; i < data.length; ++i) {
            const url = dir + "/" + data[i];
            const stat = fs.statSync(url);
            if (stat.isDirectory()) {
                this.GetAllFilesList(files, url);
            } else {
                files.push(url);
            }
        }
    }

    static RemoveDir(dir: string) {
        const data = fs.readdirSync(dir);
        for (let i = 0; i < data.length; ++i) {
            const url = dir + "/" + data[i];
            const stat = fs.statSync(url);
            if (stat.isDirectory()) {
                this.RemoveDir(url);
            } else {
                fs.unlinkSync(url);
            }
        }
        fs.rmdirSync(dir);
    }

}

class PathHelper {

    static GetFileNameWithoutExtension(fileName: string) {
        const baseFileName = path.basename(fileName);
        const extName = path.extname(fileName);
        const fileNameWithoutExtension = baseFileName.replace(extName, "");
        return fileNameWithoutExtension;
    }

    static GetRelativePath(from: string, to: string): string {
        const fromParts = from.replace(/\\/g, '/').split('/');
        const toParts = to.replace(/\\/g, '/').split('/');

        let i = 0;
        while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) {
            i++;
        }

        const upLevels = fromParts.length - i;
        const downLevels = toParts.slice(i).filter(part => part !== '').join('/');

        if (upLevels === 0) {
            return './' + downLevels;
        } else {
            return '../'.repeat(upLevels) + downLevels;
        }
    }

}

class StringHelper {

    static Format(format: string, ...args: any[]): string {
        return format.replace(/\{(\d+)\}/g, (match, index) => {
            const arg = args[parseInt(index)];
            return arg !== undefined ? arg.toString() : match;
        });
    }

}

class ExcelHelper {

    static TryGetValue(value: any) {
        if (typeof value === "string") {
            return (<string>value).trim();
        } else {
            return value;
        }
    }

}

enum ConfigType {
    c = 0,
    s = 1,
    cs = 2,
}

class HeadInfo {
    public FieldCS: string;
    public FieldDesc: string;
    public FieldName: string;
    public FieldType: string;
    public FieldIndex: number;

    constructor(cs: string, desc: string, name: string, type: string, index: number) {
        this.FieldCS = cs;
        this.FieldDesc = desc;
        this.FieldName = name;
        this.FieldType = type;
        this.FieldIndex = index;
    }
}

class Table {
    public C: boolean;
    public S: boolean;
    public Index: number = 0;
    public HeadInfos = new Dictionary<string, HeadInfo>();
}

class ExcelRange {

    [row: string]: { [col: string]: any };

}

class ExcelCellAddress {

    constructor(readonly Row: number, readonly Column: number) {

    }

}

class ExcelAddressBase {

    Start: ExcelCellAddress;
    End: ExcelCellAddress;

}

class ExcelWorksheet {

    Name: string;
    Cells: ExcelRange;
    Dimension: ExcelAddressBase;

}

class ExcelWorkBook {

    Worksheets: { [sheet: string]: ExcelWorksheet } = {};

}

class ExcelPackage {

    Workbook: ExcelWorkBook;

    constructor(readonly path: string) {
        this.Parse();
    }

    Parse() {
        const Workbook = new ExcelWorkBook();
        const path = this.path;
        const xlsxWorkbook = xlsx.readFile(path);
        for (let xlsxSheetKey in xlsxWorkbook.SheetNames) {
            let xlsxSheetName = xlsxWorkbook.SheetNames[xlsxSheetKey];
            let xlsxWorksheet = xlsxWorkbook.Sheets[xlsxSheetName];
            const Range = xlsx.utils.decode_range(xlsxWorksheet['!ref']);
            const Worksheet = new ExcelWorksheet();
            const Dimension = new ExcelAddressBase();
            Dimension.Start = new ExcelCellAddress(Range.s.r + 1, Range.s.c + 1);
            Dimension.End = new ExcelCellAddress(Range.e.r + 1, Range.e.c + 1);
            Worksheet.Name = xlsxSheetName;
            Worksheet.Dimension = Dimension;

            const Cells: ExcelRange = {};
            for (let row = 1; row <= Dimension.End.Row; ++row) {
                const CellRow = {};
                const NaturalRow = row;
                for (let col = 1; col <= Dimension.End.Column; ++col) {
                    const NaturalCol = col;
                    if (row >= Dimension.Start.Row && col >= Dimension.Start.Column) {
                        const cellAddress = xlsx.utils.encode_cell({ r: row - 1, c: col - 1 });
                        const cellValue = (xlsxWorksheet[cellAddress] && xlsxWorksheet[cellAddress].v) || '';
                        CellRow[NaturalCol] = cellValue;
                    } else {
                        CellRow[NaturalCol] = '';
                    }
                }
                Cells[NaturalRow] = CellRow;
            }

            Worksheet.Cells = Cells;
            Workbook.Worksheets[xlsxSheetName] = Worksheet;
        }
        this.Workbook = Workbook;
    }

}

export class ExcelExporter {

    private static readonly ClientClassDir = "/Codes/scripts/Codes/Model/Generate/Client/Config";
    private static readonly ServerClassDir = "/Codes/scripts/Codes/Model/Generate/Server/Config";
    private static readonly excelDir = "/Excel";
    private static readonly JsonDir = "/Config/Json/{0}/{1}";

    private static readonly clientProtoDir = "../Cocos/assets/Bundles/Config";

    private static tables = new Dictionary<string, Table>();

    private static excelPackages = new Dictionary<string, ExcelPackage>();

    private static GetTable(protoName: string): Table {
        let table = this.tables.TryGetValue(protoName);
        if (!table) {
            table = new Table();
            this.tables.Add(protoName, table);
        }
        return table;
    }

    private static GetExcelPackage(filePath: string): ExcelPackage {
        let excelPackage = this.excelPackages.TryGetValue(filePath);
        if (!excelPackage) {
            excelPackage = new ExcelPackage(filePath);
            this.excelPackages.Add(filePath, excelPackage);
        }
        return excelPackage;
    }

    private static GetClassDir(configType: ConfigType): string {
        switch (configType) {
            case ConfigType.c:
                return this.ClientClassDir;
            case ConfigType.s:
                return this.ServerClassDir;
        }
    }

    private static GetConfigType(configType: ConfigType): string {
        switch (configType) {
            case ConfigType.c:
                return "c";
            case ConfigType.s:
                return "s";
        }
    }

    public static Export(): void {
        const RootPath = path.join(__dirname, "../../../");
        const RealClientClassDir = path.join(RootPath, this.ClientClassDir);
        const RealServerClassDir = path.join(RootPath, this.ServerClassDir);
        const RealExcelDir = path.join(RootPath, this.excelDir);

        if (fs.existsSync(RealClientClassDir)) {
            FileHelper.RemoveDir(RealClientClassDir);
        }
        if (fs.existsSync(RealServerClassDir)) {
            FileHelper.RemoveDir(RealServerClassDir);
        }

        const files = FileHelper.GetAllFiles(RealExcelDir);
        for (const key in files) {
            const fullPath = files[key];
            const fileName = path.basename(fullPath);
            if (!fileName.endsWith(".xlsx") || fileName.startsWith("~$") || fileName.indexOf("#") != -1) {
                continue;
            }
            const fileNameWithoutExtension = PathHelper.GetFileNameWithoutExtension(fileName);
            let fileNameWithoutCS = fileNameWithoutExtension;
            let cs = "cs";
            if (fileNameWithoutExtension.indexOf("@") != -1) {
                const ss = fileNameWithoutExtension.split("@");
                fileNameWithoutCS = ss[0];
                cs = ss[1];
            }
            if (cs == "") {
                cs = "cs";
            }
            let protoName = fileNameWithoutCS;
            if (fileNameWithoutCS.indexOf('_') != -1) {
                protoName = fileNameWithoutCS.substring(0, fileNameWithoutCS.lastIndexOf('_'));
            }
            const p = this.GetExcelPackage(fullPath);
            const table = this.GetTable(protoName);
            if (cs.indexOf("c") != -1) {
                table.C = true;
            }
            if (cs.indexOf("s") != -1) {
                table.S = true;
            }
            this.ExportExcelClass(p, table);
        }

        for (const key of this.tables.keys()) {
            const table = this.tables.TryGetValue(key);
            if (table.C) {
                this.ExportClass(key, table.HeadInfos, ConfigType.c);
            }
            if (table.S) {
                this.ExportClass(key, table.HeadInfos, ConfigType.s);
            }
        }

        const excels = FileHelper.GetAllFiles(RealExcelDir);

        for (const key in excels) {
            const excel = excels[key];
            this.ExportExcel(excel);
        }

    }

    private static ExportExcel(fullPath: string): void {
        const RootPath = path.join(__dirname, "../../../");
        const dir = path.dirname(fullPath.replace(RootPath, ""));
        const relativePath = PathHelper.GetRelativePath(this.excelDir, dir);
        const fileName = path.basename(fullPath);
        if (!fileName.endsWith(".xlsx") || fileName.startsWith("~$") || fileName.indexOf("#") != -1) {
            return;
        }
        const fileNameWithoutExtension = PathHelper.GetFileNameWithoutExtension(fileName);
        let fileNameWithoutCS = fileNameWithoutExtension;
        let cs = "cs";
        if (fileNameWithoutExtension.indexOf("@") != -1) {
            const ss = fileNameWithoutExtension.split("@");
            fileNameWithoutCS = ss[0];
            cs = ss[1];
        }
        if (cs == "") {
            cs = "cs";
        }
        let protoName = fileNameWithoutCS;
        if (fileNameWithoutCS.indexOf('_') != -1) {
            protoName = fileNameWithoutCS.substring(0, fileNameWithoutCS.lastIndexOf('_'));
        }
        const p = this.GetExcelPackage(fullPath);
        const table = this.GetTable(protoName);

        if (cs.indexOf("c") != -1) {
            this.ExportExcelJson(p, fileNameWithoutCS, table, ConfigType.c, relativePath);
        }
        if (cs.indexOf("s") != -1) {
            this.ExportExcelJson(p, fileNameWithoutCS, table, ConfigType.s, relativePath);
        }
    }

    static ExportExcelClass(p: ExcelPackage, table: Table): void {
        const Worksheets = p.Workbook.Worksheets;
        for (let index in Worksheets) {
            let worksheet = Worksheets[index];
            if (worksheet.Name.startsWith("#")) {
                continue;
            }
            this.ExportSheetClass(worksheet, table);
        }
    }

    static ExportSheetClass(worksheet: ExcelWorksheet, table: Table): void {
        let row = 2;
        for (let col = 2; col <= worksheet.Dimension.End.Column; ++col) {
            let fieldName = ExcelHelper.TryGetValue(worksheet.Cells[row + 2][col]);
            if (!fieldName) {
                fieldName = "";
            }
            if (fieldName == "") {
                continue;
            }
            if (table.HeadInfos.ContainsKey(fieldName)) {
                continue;
            }
            let fieldCS = "";
            if (col >= 0) {
                fieldCS = ExcelHelper.TryGetValue(worksheet.Cells[row][col]).toLowerCase();
            }
            if (!fieldCS) {
                fieldCS = "";
            }
            if (fieldCS.indexOf("#") != -1) {
                table.HeadInfos[fieldName] = null;
                continue;
            }
            if (fieldCS == "") {
                fieldCS = "cs";
            }
            let oldClassField = table.HeadInfos.TryGetValue(fieldName);
            if (oldClassField) {
                if (oldClassField.FieldCS != fieldCS) {
                    console.log(`field cs not same: ${worksheet.Name} ${fieldName} oldcs: ${oldClassField.FieldCS} ${fieldCS}`);
                }
                continue;
            }
            if (col + 1 < 0) {
                continue;
            }

            const fieldDesc = ExcelHelper.TryGetValue(worksheet.Cells[row + 1][col]);
            const fieldType = ExcelHelper.TryGetValue(worksheet.Cells[row + 3][col]);

            table.HeadInfos.Add(fieldName, new HeadInfo(fieldCS, fieldDesc, fieldName, fieldType, ++table.Index));
        }
        // console.log(`ExportSheetClass => ${worksheet.Name}`);
        // console.log(table);
    }

    static ExportClass(protoName: string, classField: Dictionary<string, HeadInfo>, configType: ConfigType): void {
        const Dir = this.GetClassDir(configType);
        const RootPath = path.join(__dirname, "../../../");
        const RealDir = path.join(RootPath, Dir);
        if (!fs.existsSync(RealDir)) {
            fs.mkdirSync(RealDir, { recursive: true });
        }
        const exportPath = path.join(RealDir, `${protoName}.ts`);

        const project = new morph.Project({ compilerOptions: { target: morph.ts.ScriptTarget.ES2016 } });
        const morphSourceFile = project.createSourceFile(`${protoName}.ts`, "");

        morphSourceFile.addImportDeclaration({
            moduleSpecifier: "../../../../../Core/Module/Config/ConfigSingleton",
            namedImports: ["ConfigSingleton"],
        });

        morphSourceFile.addImportDeclaration({
            moduleSpecifier: "../../../../../Core/Module/Config/IConfig",
            namedImports: ["IConfig"],
        });

        morphSourceFile.addImportDeclaration({
            moduleSpecifier: "../../../../../Core/Object/ProtoObject",
            namedImports: ["ProtoObject"],
        });

        morphSourceFile.addImportDeclaration({
            moduleSpecifier: "../../../../../Decorator/Decorator",
            namedImports: ["ConfigAttribute", "GetTypeOf"],
        });

        morphSourceFile.addImportDeclaration({
            moduleSpecifier: "../../../../../Support/Share/Lib/Collections/List",
            namedImports: ["List"],
        });

        morphSourceFile.addImportDeclaration({
            moduleSpecifier: "../../../../../Support/Share/Lib/Dictionary",
            namedImports: ["Dictionary"],
        });

        const protoCategoryClassDecl = morphSourceFile.addClass({
            name: `${protoName}Category`
        });
        protoCategoryClassDecl.setIsExported(true);
        protoCategoryClassDecl.setExtends(`ConfigSingleton<${protoName}Category>`);
        protoCategoryClassDecl.addDecorator({
            name: "ConfigAttribute",
            arguments: []
        });

        // 添加属性和对应的 getter/setter 方法
        protoCategoryClassDecl.addProperty({
            scope: morph.Scope.Private,
            isStatic: true,
            name: `_Instance`,
            type: `${protoName}Category`, // 这里可以根据需要设置属性类型
        });

        protoCategoryClassDecl.addGetAccessor({
            isStatic: true,
            name: `Instance`,
            returnType: `${protoName}Category`, // 这里需要与属性类型保持一致
        }).setBodyText(`return ${protoName}Category._Instance;`);

        protoCategoryClassDecl.addSetAccessor({
            scope: morph.Scope.Protected,
            name: `Instance`,
            parameters: [{ name: `value`, type: `${protoName}Category` }]
        }).setBodyText(`${protoName}Category._Instance = value;`);

        protoCategoryClassDecl.addProperty({
            scope: morph.Scope.Private,
            name: `dict`,
            type: `Dictionary<number, ${protoName}>`,
            initializer: `new Dictionary<number, ${protoName}>()`
        });

        protoCategoryClassDecl.addProperty({
            scope: morph.Scope.Private,
            name: `list`,
            type: `List<${protoName}>`,
            initializer: `new List<${protoName}>()`
        });

        protoCategoryClassDecl.addMethod({
            name: `Merge`,
            returnType: `void`,
            parameters: [{ name: `o`, type: `Object` }],
            statements: `const s = o as ${protoName}Category;
            this.list.AddRange(s.list);`,
        });

        protoCategoryClassDecl.addMethod({
            name: `MergeJson`,
            returnType: `void`,
            parameters: [{ name: `o`, type: `Object` }],
            statements: `const s = o as { list: ${protoName}[] };
            const len = s.list.length;
            for (let i = 0; i < len; ++i) {
                const jsonObject = s.list[i];
                const config = ${protoName}.Create(jsonObject);
                this.list.Add(config);
            }`,
        });

        protoCategoryClassDecl.addMethod({
            name: `ProtoEndInit`,
            returnType: `void`,
            parameters: [],
            statements: `for (const key in this.list.ToArray()) {
                const index = parseInt(key);
                const config = this.list.Get(index);
                config.AfterEndInit();
                this.dict.Add(config.Id, config);
            }
            this.list.Clear();
            this.AfterEndInit();`,
        });

        protoCategoryClassDecl.addMethod({
            name: `Get`,
            returnType: `${protoName}`,
            parameters: [{ name: `id`, type: `number` }],
            statements: `const item = this.dict.TryGetValue(id);
            if (item == null) {
                throw new Error(\`配置找不到，配置表名: \${GetTypeOf(${protoName})}，配置id: \${id}\`);
            }
            return item;`,
        });

        protoCategoryClassDecl.addMethod({
            name: `Contain`,
            returnType: `boolean`,
            parameters: [{ name: `id`, type: `number` }],
            statements: `return this.dict.ContainsKey(id);`,
        });

        protoCategoryClassDecl.addMethod({
            name: `GetAll`,
            returnType: `Dictionary<number, ${protoName}>`,
            parameters: [],
            statements: `return this.dict;`,
        });

        protoCategoryClassDecl.addMethod({
            name: `GetOne`,
            returnType: `${protoName}`,
            parameters: [],
            statements: `if (this.dict == null || this.dict.Count <= 0) {
                return null;
            }
            return this.dict.Values().next().value;`,
        });

        const protoClassDecl = morphSourceFile.addClass({
            name: protoName
        });
        protoClassDecl.setIsExported(true);
        protoClassDecl.setExtends(`ProtoObject`);
        protoClassDecl.addImplements(`IConfig`);

        protoClassDecl.addMethod({
            isStatic: true,
            name: `Create`,
            returnType: `${protoName}`,
            parameters: [{ name: `o`, type: `Object` }],
            statements: `const config = new ${protoName}();
            for (let key in o) {
                let oldKey = key;
                if (key === "_t") {
                    continue;
                }
                if (key === "_Id" || key === "_id" || key === "id") {
                    key = "Id";
                }
                config[key] = o[oldKey];
            }
            return config;`,
        });

        for (const key of classField.keys()) {
            const headInfo = classField.TryGetValue(key);
            if (headInfo == null) {
                continue;
            }
            if (configType != ConfigType.cs && headInfo.FieldCS.indexOf(this.GetConfigType(configType)) == -1) {
                continue;
            }
            protoClassDecl.addProperty({
                scope: morph.Scope.Public,
                name: headInfo.FieldName,
                type: this.ConvertType(headInfo.FieldType)
            });
        }

        morphSourceFile.formatText();

        const newFileContent = morphSourceFile.getFullText();

        const exportFolder = path.dirname(exportPath);
        if (fs.existsSync(exportPath)) {
            fs.rmSync(exportPath);
        }
        if (!fs.existsSync(exportFolder)) {
            fs.mkdirSync(exportFolder, { recursive: true });
        }
        // 写入新的输出文件到指定目录下 
        fs.writeFileSync(exportPath, newFileContent);
    }

    //#region 到处json

    static ExportExcelJson(p: ExcelPackage, name: string, table: Table, configType: ConfigType, relativeDir: string): void {
        const RootPath = path.join(__dirname, "../../../");
        let lineList: string[] = [];
        lineList.push("{\"list\":[\n");
        const Worksheets = p.Workbook.Worksheets;
        for (let key in Worksheets) {
            let worksheet = Worksheets[key];
            if (worksheet.Name.startsWith("#")) {
                continue;
            }
            this.ExportSheetJson(worksheet, name, table.HeadInfos, configType, lineList);
        }
        lineList.pop();//去除数组最后一个元素的 (,) 符号
        lineList.push("}\n");
        lineList.push("]}\n");
        const Dir = StringHelper.Format(this.JsonDir, this.GetConfigType(configType), relativeDir);
        const RealDir = path.join(RootPath, Dir);
        if (!fs.existsSync(RealDir)) {
            fs.mkdirSync(RealDir, { recursive: true });
        }
        const jsonPath = path.join(RealDir, `${name}.txt`);
        fs.writeFileSync(jsonPath, lineList.join(""));
    }

    static ExportSheetJson(worksheet: ExcelWorksheet, name: string, classField: Dictionary<string, HeadInfo>, configType: ConfigType, lineList: string[]): void {
        const configTypeStr = this.GetConfigType(configType);
        for (let row = 6; row <= worksheet.Dimension.End.Row; ++row) {
            let prefix = ExcelHelper.TryGetValue(worksheet.Cells[row][2]);
            if (!prefix) {
                prefix = "";
            }
            if (prefix.indexOf("#") != -1) {
                continue;
            }
            if (prefix == "") {
                prefix = "cs";
            }
            if (configType != ConfigType.cs && prefix.indexOf(configTypeStr) == -1) {
                continue;
            }
            lineList.push("{");
            lineList.push(`\"_t\":\"${name}\"`);
            for (let col = 3; col <= worksheet.Dimension.End.Column; ++col) {
                const fieldName = ExcelHelper.TryGetValue(worksheet.Cells[4][col]);
                if (!classField.ContainsKey(fieldName)) {
                    continue;
                }
                const headInfo = classField.TryGetValue(fieldName);
                if (headInfo == null) {
                    continue;
                }
                if (configType != ConfigType.cs && headInfo.FieldCS.indexOf(configTypeStr) == -1) {
                    continue;
                }
                let fieldN = headInfo.FieldName;
                if (fieldN == "Id") {
                    fieldN = "_id";
                }
                lineList.push(`,\"${fieldN}\":${this.Convert(headInfo.FieldType, ExcelHelper.TryGetValue(worksheet.Cells[row][col]))}`);
            }
            lineList.push("},\n");
        }
    }

    private static Convert(type: string, value: string): string {
        switch (type) {
            case "uint[]":
            case "int[]":
            case "int32[]":
            case "long[]":
                return `[${value}]`;
            case "string[]":
            case "int[][]":
                return `[${value}]`;
            case "int":
            case "uint":
            case "int32":
            case "int64":
            case "long":
            case "float":
            case "double":
                if (value == "") {
                    return "0";
                }
                return value;
            case "string":
                if (typeof value === "string") {
                    value = value.replace("\\", "\\\\");
                    value = value.replace("\"", "\\\"");
                }
                return `\"${value}\"`;
            default:
                throw new Error(`不支持此类型: ${type}`);
        }
    }

    private static ConvertType(type: string): string {
        switch (type) {
            case "uint[]":
            case "int[]":
            case "int32[]":
            case "long[]":
                return `number[]`;
            case "string[]":
                return type;
            case "int[][]":
                return `number[][]`;
            case "int":
            case "uint":
            case "int32":
            case "int64":
            case "long":
            case "float":
            case "double":
                return `number`;
            case "string":
                return type;
            default:
                throw new Error(`不支持此类型: ${type}`);
        }
    }

    //#endregion 

}

ExcelExporter.Export();